/*
 * MIT License
 *
 * Copyright (c) 2020 wen.gu <454727014@qq.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

 /***************************************************************************
 * Name: arg_parser.h
 *
 * Purpose: a command line arguments parser implementation
 *
 * Developer:
 *   wen.gu , 2021-09-16
 *
 * TODO:
 *
 ***************************************************************************/

 /******************************************************************************
 **    INCLUDES
 ******************************************************************************/
#ifndef __ICPP_ARG_PARSER_H__
#define __ICPP_ARG_PARSER_H__

#include <iostream>

#include <string>
#include <functional>
#include <type_traits>
#include <set>
#include <initializer_list>
#include <vector>
#include <sstream>
#include <algorithm>
#include <iomanip>

#ifdef __GNUC__
#include <cxxabi.h>
#endif // __GNUC__


namespace icpp
{
namespace core
{
/******************************************************************************
 **    MACROS
 ******************************************************************************/
#define ARG_NO_SHORT_NAME '\0'
#define ARG_NO_LONG_NAME  ""
#define ARG_SHORT_NAME_PREFIX ((char)'-')
#define ARG_LONG_NAME_PREFIX "--"

#define FUNC_TRACE()  std::cout<<"["<<__FUNCTION__ << "." << __LINE__<<"]:" << std::endl;
#define RECORD_ERR(err) recordError(err, __FUNCTION__, __LINE__)
 /******************************************************************************
 **    TYPE DEFINITIONS
 ******************************************************************************/
enum class ArgumentParamType: uint8_t
{
    kParamNone = 0,  /** the command argument needn't param */
    kParamRequired,  /** the command argument must set param */
    kParamOptional,  /** the param of command argument is optional, if not set, then here hase default value */
};

enum class ArgumentParseError: uint8_t
{
    kSuccess = 0,
    kUndefined, 
    kDoCommand,  /** used only addWithParamNoneHandler */
    kBadParameter,
    kMandatoryArgumentNotFound,
    kNotFoundLongArgument,
    kNotFoundShortArgument,
    kParseParamFailed,
    kMustTakeParam,
    kInvalidParamValue,
    kBadArgument,
};

 /******************************************************************************
 **    CLASSES/FUNCTIONS DEFINITIONS
 ******************************************************************************/
namespace internal_impl
{

template<class ParamType>
bool ParamString2Value(const std::string& val_str, ParamType& param)
{
    std::istringstream iss(val_str);

    if ((iss >> param) && iss.eof())
    {
        return true;
    }

    return false;
}

template<>
bool ParamString2Value<std::string>(const std::string& val_str, std::string& param)
{
    param = val_str;
    return true;
}

class ArgumentReader final
{
public:
    ArgumentReader(int argc, const char** argv)
    :cur_idx_(0), argc_(argc), argv_(argv)
    {/** todo something */}
public:
    const char* nextToken(){ return (cur_idx_ < argc_) ? argv_[cur_idx_++] : nullptr; }
private:
    int cur_idx_;
    int argc_;
    const char** argv_;

};

template<class ParamType>
inline std::string ParamTypeNmae()
{
    std::string type_name = typeid(ParamType).name();
#ifdef __GNU__
    int status=0;
    char *p = abi::__cxa_demangle(type_name.c_str(), 0, 0, &status);
    type_name = p;
    free(p);
#endif
    return type_name;
}

template <>
inline std::string ParamTypeNmae<std::string>()
{
  return "string";
}

template<class ParamType>
using ValueChecker = std::function<bool(const ParamType&)>;

template<class ParamType>
class OptionValue
{
private:
    bool has_value_ = false;
    ParamType value_ = ParamType();
public:
    OptionValue(){/** todo something*/}
    OptionValue(const ParamType& val): value_(val), has_value_(true){/** todo something*/}
    bool has_value() const { return has_value_;}
    const ParamType& value() const { return value_;}
};

template<>
class OptionValue<std::string>
{
private:
    bool has_value_ = false;
    std::string value_ = std::string();
public:
    OptionValue(){/** todo something*/}
    OptionValue(const std::string& val): value_(val), has_value_(true){/** todo something*/}
    OptionValue(const char* val): value_(val), has_value_(true){/** todo something*/}
    bool has_value() const { return has_value_;}
    const std::string& value() const { return value_;}
};

} /** end of namespace internal_impl*/


template<typename ParamType>
internal_impl::ValueChecker<ParamType> ParamRange(ParamType min, ParamType max)
{
    return [min, max](const ParamType& val) -> bool { return ((min <= val) && (val <= max));};
}

template<typename ParamType>
internal_impl::ValueChecker<ParamType> ParamOneOf(std::initializer_list<ParamType> l)
{
    using ParamArray = std::set<ParamType>;
    ParamArray params(l);
    return [params](const ParamType& val) -> bool 
    {
        for (const auto& it: params)
        {
            if (it == val)
            {
                return true;
            }
        }

        /**if nothing in the params_, mean that haven't limit, so always true */
        return (params.size() == 0) ? true: false;        
    };
}


class ArgParser
{
public:
    using OnArgExistHandler = std::function<void(bool)>;
    using PrintHelpHandler = std::function<void(const std::string&)>;
    //using ArgumentParseError = internal_impl::ArgumentParseError;

    /** the callback function to process the value of argument param 
     *  return: true: process param value success, false: failed
     */
    using OnParamHandler = std::function<ArgumentParseError(const std::string&)>;
   
    struct ArgInfo
    {
        bool is_mandatory = false; /** true: the argument is mandatory, false: the argument is optional */
        bool has_parameter = false; /** true: the argument has paramter, false: the argument needn't param */
        char arg_short_name = ARG_NO_SHORT_NAME;
        std::string arg_name;
        std::string arg_description; /** full description for current argument */
        std::string arg_short_description; /** a short description for current argument(used only ,mandatory argument, else set as empty) */
        OnParamHandler on_param; 
    };

    using ArgumentArray = std::vector<ArgInfo>;
    using ErrorArray = std::vector<std::string>; 

public:
    ArgParser() {/** todo something */}

    virtual ~ArgParser() {/** todo something */ }

public:
    ArgParser(const ArgParser& other) = delete;
    ArgParser& operator=(const ArgParser& other) = delete;

public:
    /** add argument with param process callback handler */
    template<class ParamType>
    bool addWithParamHandler(const std::string& arg_name,         /** the long name of current argument */
                             char arg_short_name,                 /** the short name of current argument */
                             const std::string& arg_description,  /** description for current argument */ 
                             const std::string& arg_short_description,  /** short description for current argument(used only mandatory argument, else set as empty) */ 
                             bool is_mandatory,                   /** true: the argument is mandatory, false: the argument is optional */
                             bool has_parameter,                  /** true: the argument has paramter, false: the argument needn't param */ 
                             std::function<ArgumentParseError(const ParamType&)> on_param) /** a callback to process parameter value */

    {
        if (!((!arg_name.empty()) || (arg_short_name != ARG_NO_SHORT_NAME)))
        {
            RECORD_ERR("add agrument for command line at least have a short name or long name");
            return false;
        }

        return add(arg_name, arg_short_name, arg_description, arg_short_description, is_mandatory, has_parameter,  [on_param](const std::string& param_val) -> ArgumentParseError
        {
            ParamType param;
            if (internal_impl::ParamString2Value(param_val, param))
            {
                return on_param(param);
            }

            return ArgumentParseError::kParseParamFailed;
        });
    } 

    bool addWithParamNoneHandler(const std::string& arg_name,           /** the long name of current argument */
                          char arg_short_name,                   /** the short name of current argument */
                          const std::string& arg_description,     /** description for current argument */  
                          std::function<ArgumentParseError()> on_argument)
    {
        if (!((!arg_name.empty()) || (arg_short_name != ARG_NO_SHORT_NAME) || (on_argument == nullptr)))
        {
            RECORD_ERR("add agrument for command line at least have a short name or long name, and valid callbak handler");
            return false;
        }

        return add( arg_name, arg_short_name, arg_description + "(None)", "", false, false, [on_argument](const std::string& param_val) -> ArgumentParseError
        {            
            return on_argument(); /** todo refine me?? */
        });        
    }

    /** add argument which needn't take param */
    bool addWithParamNone(const std::string& arg_name,           /** the long name of current argument */
                          char arg_short_name,                   /** the short name of current argument */
                          const std::string& arg_description,     /** description for current argument */  
                          bool & has_got)                        /** true: got this argument, false: current argument not present */    
    {
        has_got = false;  /** set default status as not got yet */
        return addWithParamNoneHandler( arg_name, arg_short_name, arg_description,  [&has_got]() -> ArgumentParseError
        {
            has_got = true;
            return ArgumentParseError::kSuccess;
        });
    }                        

    /** add argument which must take param, but param value limit in a range */
    template<class ParamType>
    bool addWithParamRequired(const std::string& arg_name,                  /** the long name of current argument */
                              char arg_short_name,                          /** the short name of current argument */
                              const std::string& arg_description,           /** description for current argument */  
                              bool is_mandatory,                            /** true: the argument is mandatory, false: the argument is optional */
                              ParamType& param,                             /** output the param value of current argument */
                              internal_impl::OptionValue<ParamType>&& default_val = internal_impl::OptionValue<ParamType>(),   /** the default parameter value for current argument*/
                              internal_impl::ValueChecker<ParamType> checker = nullptr)   /** use to check valid for parameter value */ 
                     
    {
        if (!((!arg_name.empty()) || (arg_short_name != ARG_NO_SHORT_NAME)))
        {
            RECORD_ERR("add agrument for command line at least have a short name or long name");
            return false;
        }

        if (default_val.has_value())
        {
            if ((checker != nullptr) && !checker(default_val.value()))
            {
                RECORD_ERR("invalid default parameter value for option --" + arg_name + "or -" + arg_short_name);
                return false;
            }    
            
            param = default_val.value(); 
        }


        std::string param_type_name = internal_impl::ParamTypeNmae<ParamType>();
        std::string desc= makeDescription(param_type_name, arg_description, default_val);
        std::string short_desc = is_mandatory ? makeShortDescription(arg_name, arg_short_name, param_type_name) : "";        
        
        return addWithParamHandler<ParamType>(arg_name, arg_short_name, desc, short_desc, is_mandatory, true, [&param, checker, arg_name, arg_short_name, this](const ParamType& param_val) -> ArgumentParseError
        {
            if ((checker != nullptr) &&  !checker(param_val))
            {    
                if (!arg_name.empty())
                {
                    std::ostringstream oss;
                    oss << "option value is invalid: --" << arg_name << "=" << param_val;
                    recordError(oss.str()); 
                }
                else
                {
                    std::ostringstream oss;
                    oss << "option value is invalid: -" << arg_short_name << " " << param_val;
                    recordError(oss.str());                     
                }
                           
                return ArgumentParseError::kInvalidParamValue;
            }   

            param = param_val;
            return ArgumentParseError::kSuccess;
        });
    }    

public:
    /**
     * include_prog_name: true: the first arg is program name(or process name), false: the all args are short/long arg
    */
    ArgumentParseError onParse(int argc, char** argv, bool include_prog_name = true)
    {
        if ((argc < 1) || (argv == nullptr))
        {
            RECORD_ERR("argument number must be longer than 0");
            return ArgumentParseError::kBadParameter;
        }

        internal_impl::ArgumentReader arg_reader(argc, (const char**)argv);

        const char* cur_token = arg_reader.nextToken();

        if (include_prog_name)
        {/** todo refine me? */
            program_name_ = cur_token;
            cur_token = arg_reader.nextToken();
        }

        if (cur_token == nullptr)
        {
            showHelp();
            return ArgumentParseError::kDoCommand;            
        }

        while (cur_token)
        {
            if (cur_token[0] == ARG_SHORT_NAME_PREFIX)
            {
                if (cur_token[1] == ARG_SHORT_NAME_PREFIX) /** mean that argument with long name: --xxx */
                {
                    if (cur_token[2] == '\0') /** a long name argument at least 3 char: --x */
                    {
                        RECORD_ERR("syntax error, a long name argument at least 3 char: --x");
                        return ArgumentParseError::kBadArgument;
                    }

                    ArgumentParseError ret = onParseLongNameArgument(arg_reader, cur_token + 2); /** +2 to skip "--" */
                    if (ArgumentParseError::kSuccess != ret)
                    {
                        return ret;
                    }
                }
                else if (cur_token[1] == '\0')
                {
                    RECORD_ERR("invalid argument: " + std::string(cur_token));
                    return ArgumentParseError::kBadArgument; /** todo refine me */                   
                }
                else /** this case mean that a short name argument */
                {
                    ArgumentParseError ret = onParseShortNameArgument(arg_reader, cur_token + 1); /** +1 to skip '-' */
                    if (ArgumentParseError::kSuccess != ret)
                    {
                        return ret;
                    }                    
                }
            }
            else
            {
                RECORD_ERR("invalid argument: " + std::string(cur_token));
                return ArgumentParseError::kUndefined; /** todo refine me */
            }

            cur_token = arg_reader.nextToken();
        }

        return ArgumentParseError::kSuccess;
    }

public:
    void showHelp()
    {
        std::cout << this->getHelpInfo();
    }

    void showHelp(PrintHelpHandler on_print)
    {
        if (on_print)
        {
            on_print(getHelpInfo());
        }
    }

    std::string getHelpInfo()
    {
        std::ostringstream usage;
        std::ostringstream mandatory;
        std::ostringstream optional;
        usage << "usage: "<<program_name_ << " ";
        size_t long_name_max_width = 0;
        for (const auto& it : arguments_)
        {
            long_name_max_width = std::max(long_name_max_width, it.arg_name.size());
        }

       // std::cout << " max_width: " << long_name_max_width + 4 << std::endl;
        for (const auto& it : arguments_)
        {
            if (it.is_mandatory)
            {
                mandatory << it.arg_short_description << " ";            
            }

            if (it.arg_short_name != ARG_NO_SHORT_NAME)
            {
                optional << "  -" << it.arg_short_name<< ", ";
            }
            else
            {
                optional << "      ";
            }
            
            if (!it.arg_name.empty())
            {
                optional << "--" << it.arg_name;
            }

            optional << std::string(long_name_max_width + 4 - it.arg_name.length(), ' ') << it.arg_description << std::endl;            
        }

        usage << mandatory.str() << " [options] ..." << std::endl;
        usage << "options:" << std::endl;
        usage << optional.str() << std::endl;
        return usage.str();
    }

protected:

    /** add argument with param process callback handler(aways default type: std::string) */
    virtual bool add(const std::string& arg_name,               /** the long name of current argument */
                     char arg_short_name,                       /** the short name of current argument */
                     const std::string& arg_description,        /** description for current argument */ 
                     const std::string& arg_short_description,  /** short description for current argument(used only mandatory argument, else set as empty) */   
                     bool is_mandatory,                         /** true: the argument is mandatory, false: the argument is optional */
                     bool has_parameter,                        /** true: the argument has paramter, false: the argument needn't param */                     
                     OnParamHandler on_param)                   /** callback function to process the value of param */
    {
        if (checkExist(arg_name, arg_short_name))
        {
            std::string argn = arg_name.empty() ? ("-" + std::string(arg_short_name, 1)) : ("--" + arg_name);
            RECORD_ERR("argument: " + argn + " already exist");
            return false;
        }

        ArgInfo ai;
        ai.is_mandatory = is_mandatory;
        ai.arg_description = arg_description;
        ai.arg_name = arg_name;
        ai.arg_short_description = arg_short_description;
        ai.arg_short_name = arg_short_name;
        ai.has_parameter = has_parameter;
        ai.on_param = on_param;
        
        arguments_.push_back(ai); 

        return true;       
    }    

    void separateArgument(const std::string& arg, std::string& arg_name, std::string& param_str)
    {
        size_t pos = arg.find("=");

        if (pos == std::string::npos)
        {
            arg_name = arg;
        }
        else
        {
            arg_name = arg.substr(0, pos);
            param_str = arg.substr(pos);         
        }
    }

    ArgumentParseError onParseParam(const std::string& arg_name, ArgInfo* ai, internal_impl::ArgumentReader& arg_reader, std::string& param_str)
    {
        if (!ai->has_parameter) /** if argument define without parameter */
        {
            return ai->on_param ? ai->on_param("") : ArgumentParseError::kUndefined;
        }

        if (param_str.empty())
        {
            const char* next_token = arg_reader.nextToken();
            if (next_token[0] != ARG_SHORT_NAME_PREFIX) /** mean that a string not start with '-' or '--' is a valid parameter */
            {
                param_str = next_token;
            }            
        }

        if (param_str.empty())
        {
            RECORD_ERR("invalid option: " + arg_name);
            return ArgumentParseError::kMustTakeParam;               
        }
        return ai->on_param ? ai->on_param(param_str) : ArgumentParseError::kUndefined;        
    }

    ArgumentParseError onParseLongNameArgument(internal_impl::ArgumentReader& arg_reader, const char* cur_token)
    {
        std::string long_name;
        std::string param_str;
        separateArgument(cur_token, long_name, param_str);
        ArgInfo* ai = getArgInfo(long_name);
        if (!ai)
        {
            RECORD_ERR("undefined option: --" + long_name);
            return ArgumentParseError::kNotFoundLongArgument;
        }

        return onParseParam("--" + long_name, ai, arg_reader, param_str);
    }

    ArgumentParseError onParseShortNameArgument(internal_impl::ArgumentReader& arg_reader, const char* cur_token)
    {
        ArgInfo* ai = getArgInfo(cur_token[0]);
        if (!ai)
        {
            RECORD_ERR("undefined option: -" + std::string(cur_token));
            return ArgumentParseError::kNotFoundLongArgument;
        }

        std::string param_str = (cur_token[1] == '=') ? (cur_token + 2) : "";

        return onParseParam("-" + std::string(cur_token), ai, arg_reader, param_str);
    }


    ArgInfo* getArgInfo(const std::string& long_name)
    {
        for (auto& it: arguments_)
        {
            if (it.arg_name == long_name)
            {
                return &it;
            }
        }

        return nullptr;
    } 

    ArgInfo* getArgInfo(char short_name)
    {
        for (auto& it: arguments_)
        {
            if (it.arg_short_name == short_name)
            {
                return &it;
            }
        }

        return nullptr;
    } 

    template<class ParamType>
    std::string makeDescription(const std::string& param_type, const std::string& input_description, const internal_impl::OptionValue<ParamType>& default_val)
    {
        std::ostringstream oss;
        oss << input_description << "(" << param_type ;
        if (default_val.has_value())
        {
            oss << " [=" << default_val.value() << "])";
        }
        else
        {
           oss << ")";
        }  

        return std::move(oss.str()); 
    }

    std::string makeShortDescription(const std::string& arg_name, char arg_short_name, const std::string& param_type)
    {
        std::ostringstream oss;
        if (!arg_name.empty())
        {
            oss << "--" << arg_name << "=" << param_type;
        }
        else 
        {
            oss << "-" << arg_short_name << " " << param_type;
        }

        return std::move(oss.str());
    }

private:
    void clearError(){ errors_.clear();}
    void recordError(const std::string& err,const std::string& func, int line) 
    {
        std::ostringstream osst;
        osst << "[" << func << "." << line << "]:" << err;
        errors_.push_back(osst.str());
    }

    void recordError(const std::string& err)
    {
        errors_.push_back(err);
    }
    bool checkExist(const std::string& arg_name, char arg_short_name)
    {
        for (const auto& it: arguments_)
        {
            if ((!it.arg_name.empty() && (it.arg_name == arg_name)) || ((it.arg_short_name != ARG_NO_SHORT_NAME) && (it.arg_short_name == arg_short_name)))
            {
                return true;
            }
        }

        return false;
    }
public:    
    std::string error() const { return (errors_.size() > 0) ? errors_[0] : "";}
    std::string fullError() 
    {
        std::ostringstream osst;
        for (const auto& it: errors_)
        {
            osst << it << std::endl;
        }
        
        return osst.str();
    }
private:
    std::string program_name_;
    ArgumentArray arguments_;
    ErrorArray errors_;
};


} /** namespace core*/
} /** namespace icpp*/
#endif /** !__ICPP_ARG_PARSER_H__ */
